package gitbucket.core.service

import fr.brouillard.oss.security.xhub.XHub
import fr.brouillard.oss.security.xhub.XHub.{XHubConverter, XHubDigest}
import gitbucket.core.api._
import gitbucket.core.model.{
    Account,
    AccountWebHook,
    AccountWebHookEvent,
    CommitComment,
    Issue,
    IssueComment,
    Label,
    PullRequest,
    RepositoryWebHook,
    RepositoryWebHookEvent,
    WebHook
}
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
import org.apache.http.client.utils.URLEncodedUtils
import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util.{HttpClientUtil, RepositoryName, StringUtil}
import gitbucket.core.service.RepositoryService.RepositoryInfo
import org.apache.http.NameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.message.BasicNameValuePair
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ObjectId
import org.slf4j.LoggerFactory

import scala.concurrent._
import scala.util.{Failure, Success}
import org.apache.http.HttpRequest
import org.apache.http.HttpResponse
import gitbucket.core.model.WebHookContentType
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.view.helpers.getApiMilestone
import org.apache.http.client.entity.EntityBuilder
import org.apache.http.entity.ContentType

trait WebHookService {
    import WebHookService._

    private val logger = LoggerFactory.getLogger(classOf[WebHookService])

    /** get All WebHook informations of repository */
    def getWebHooks(owner: String, repository: String)(implicit
        s: Session
    ): List[(RepositoryWebHook, Set[WebHook.Event])] = RepositoryWebHooks
        .filter(_.byRepository(owner, repository)).join(RepositoryWebHookEvents).on { (w, t) =>
            t.byRepositoryWebHook(w)
        }.map { case (w, t) => w -> t.event }.list.groupBy(_._1).view.mapValues(_.map(_._2).toSet)
        .toList.sortBy(_._1.url)

    /** get All WebHook informations of repository event */
    def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit
        s: Session
    ): List[RepositoryWebHook] = RepositoryWebHooks.filter(_.byRepository(owner, repository))
        .join(RepositoryWebHookEvents).on { (wh, whe) => whe.byRepositoryWebHook(wh) }
        .filter { case (wh, whe) => whe.event === event.bind }.map { case (wh, whe) => wh }.list
        .distinct

    /** get All WebHook information from repository to url */
    def getWebHook(owner: String, repository: String, url: String)(implicit
        s: Session
    ): Option[(RepositoryWebHook, Set[WebHook.Event])] = RepositoryWebHooks
        .filter(_.byRepositoryUrl(owner, repository, url)).join(RepositoryWebHookEvents)
        .on { (w, t) => t.byRepositoryWebHook(w) }.map { case (w, t) => w -> t.event }.list
        .groupBy(_._1).view.mapValues(_.map(_._2).toSet).headOption

    /** get All WebHook informations of repository */
    def getWebHookById(id: Int)(implicit
        s: Session
    ): Option[(RepositoryWebHook, Set[WebHook.Event])] = RepositoryWebHooks.filter(_.byId(id))
        .join(RepositoryWebHookEvents).on { (w, t) => t.byRepositoryWebHook(w) }
        .map { case (w, t) => w -> t.event }.list.groupBy(_._1).view.mapValues(_.map(_._2).toSet)
        .toList.headOption

    def addWebHook(
        owner: String,
        repository: String,
        url: String,
        events: Set[WebHook.Event],
        ctype: WebHookContentType,
        token: Option[String]
    )(implicit s: Session): Unit = {
        RepositoryWebHooks insert RepositoryWebHook(
          userName = owner,
          repositoryName = repository,
          url = url,
          ctype = ctype,
          token = token
        )
        events.map { (event: WebHook.Event) =>
            RepositoryWebHookEvents insert RepositoryWebHookEvent(owner, repository, url, event)
        }
    }

    def updateWebHook(
        owner: String,
        repository: String,
        url: String,
        events: Set[WebHook.Event],
        ctype: WebHookContentType,
        token: Option[String]
    )(implicit s: Session): Unit = {
        RepositoryWebHooks.filter(_.byRepositoryUrl(owner, repository, url))
            .map(w => (w.ctype, w.token)).update((ctype, token))
        RepositoryWebHookEvents.filter(_.byRepositoryWebHook(owner, repository, url)).delete
        events.map { (event: WebHook.Event) =>
            RepositoryWebHookEvents insert RepositoryWebHookEvent(owner, repository, url, event)
        }
    }

    def updateWebHookByApi(
        id: Int,
        owner: String,
        repository: String,
        url: String,
        events: Set[WebHook.Event],
        ctype: WebHookContentType,
        token: Option[String]
    )(implicit s: Session): Unit = {
        RepositoryWebHooks.filter(_.byId(id)).map(w => (w.url, w.ctype, w.token))
            .update((url, ctype, token))
        RepositoryWebHookEvents.filter(_.byRepositoryWebHook(owner, repository, url)).delete
        events.map { (event: WebHook.Event) =>
            RepositoryWebHookEvents insert RepositoryWebHookEvent(owner, repository, url, event)
        }
    }

    // Records in WEB_HOOK_EVENT will be deleted automatically by cascaded constraint
    def deleteWebHook(owner: String, repository: String, url: String)(implicit s: Session): Unit =
        RepositoryWebHooks.filter(_.byRepositoryUrl(owner, repository, url)).delete

    // Records in WEB_HOOK_EVENT will be deleted automatically by cascaded constraint
    def deleteWebHookById(id: Int)(implicit s: Session): Unit = RepositoryWebHooks
        .filter(_.byId(id)).delete

    /** get All AccountWebHook informations of user */
    def getAccountWebHooks(owner: String)(implicit
        s: Session
    ): List[(AccountWebHook, Set[WebHook.Event])] = AccountWebHooks.filter(_.byAccount(owner))
        .join(AccountWebHookEvents).on { (w, t) => t.byAccountWebHook(w) }.map { case (w, t) =>
            w -> t.event
        }.list.groupBy(_._1).view.mapValues(_.map(_._2).toSet).toList.sortBy(_._1.url)

    /** get All AccountWebHook informations of repository event */
    def getAccountWebHooksByEvent(owner: String, event: WebHook.Event)(implicit
        s: Session
    ): List[AccountWebHook] = AccountWebHooks.filter(_.byAccount(owner)).join(AccountWebHookEvents)
        .on { (wh, whe) => whe.byAccountWebHook(wh) }.filter { case (wh, whe) =>
            whe.event === event.bind
        }.map { case (wh, whe) => wh }.list.distinct

    /** get All AccountWebHook information from repository to url */
    def getAccountWebHook(owner: String, url: String)(implicit
        s: Session
    ): Option[(AccountWebHook, Set[WebHook.Event])] = AccountWebHooks
        .filter(_.byPrimaryKey(owner, url)).join(AccountWebHookEvents).on { (w, t) =>
            t.byAccountWebHook(w)
        }.map { case (w, t) => w -> t.event }.list.groupBy(_._1).view.mapValues(_.map(_._2).toSet)
        .headOption

    def addAccountWebHook(
        owner: String,
        url: String,
        events: Set[WebHook.Event],
        ctype: WebHookContentType,
        token: Option[String]
    )(implicit s: Session): Unit = {
        AccountWebHooks insert AccountWebHook(owner, url, ctype, token)
        events.map { (event: WebHook.Event) =>
            AccountWebHookEvents insert AccountWebHookEvent(owner, url, event)
        }
    }

    def updateAccountWebHook(
        owner: String,
        url: String,
        events: Set[WebHook.Event],
        ctype: WebHookContentType,
        token: Option[String]
    )(implicit s: Session): Unit = {
        AccountWebHooks.filter(_.byPrimaryKey(owner, url)).map(w => (w.ctype, w.token))
            .update((ctype, token))
        AccountWebHookEvents.filter(_.byAccountWebHook(owner, url)).delete
        events.map { (event: WebHook.Event) =>
            AccountWebHookEvents insert AccountWebHookEvent(owner, url, event)
        }
    }

    def deleteAccountWebHook(owner: String, url: String)(implicit s: Session): Unit =
        AccountWebHooks.filter(_.byPrimaryKey(owner, url)).delete

    def callWebHookOf(
        owner: String,
        repository: String,
        event: WebHook.Event,
        settings: SystemSettings
    )(makePayload: => Option[WebHookPayload])(implicit s: Session, c: JsonFormat.Context): Unit = {
        val webHooks = getWebHooksByEvent(owner, repository, event)
        if (webHooks.nonEmpty) { makePayload.foreach(callWebHook(event, webHooks, _, settings)) }
        val accountWebHooks = getAccountWebHooksByEvent(owner, event)
        if (accountWebHooks.nonEmpty) {
            makePayload.foreach(callWebHook(event, accountWebHooks, _, settings))
        }
    }

    private def validateTargetAddress(settings: SystemSettings, url: String): Boolean = {
        val host = new java.net.URL(url).getHost

        !settings.webHook.blockPrivateAddress || !HttpClientUtil.isPrivateAddress(host) ||
        settings.webHook.whitelist.exists(range => HttpClientUtil.inIpRange(range, host))
    }

    def callWebHook(
        event: WebHook.Event,
        webHooks: List[WebHook],
        payload: WebHookPayload,
        settings: SystemSettings
    )(implicit
        c: JsonFormat.Context
    ): List[(WebHook, String, Future[HttpRequest], Future[HttpResponse])] = {
        import org.apache.http.impl.client.HttpClientBuilder
        import ExecutionContext.Implicits.global // TODO Shouldn't use the default execution context
        import org.apache.http.protocol.HttpContext
        import org.apache.http.client.methods.HttpPost

        if (webHooks.nonEmpty) {
            val json = JsonFormat(payload)

            webHooks.map { webHook =>
                val reqPromise = Promise[HttpRequest]()
                val f = Future {
                    val itcp = new org.apache.http.HttpRequestInterceptor {
                        def process(res: HttpRequest, ctx: HttpContext): Unit = {
                            reqPromise.success(res)
                        }
                    }
                    try {
                        if (!validateTargetAddress(settings, webHook.url)) {
                            throw new IllegalArgumentException(s"Illegal address: ${webHook.url}")
                        }
                        val httpClient = HttpClientBuilder.create.useSystemProperties
                            .addInterceptorLast(itcp).build
                        logger.debug(s"start web hook invocation for ${webHook.url}")
                        val httpPost = new HttpPost(webHook.url)
                        logger.debug(s"Content-Type: ${webHook.ctype.ctype}")
                        httpPost.addHeader("Content-Type", webHook.ctype.ctype)
                        httpPost.addHeader("X-Github-Event", event.name)
                        httpPost
                            .addHeader("X-Github-Delivery", java.util.UUID.randomUUID().toString)

                        webHook.ctype match {
                            case WebHookContentType.FORM => {
                                val params: java.util.List[NameValuePair] =
                                    new java.util.ArrayList()
                                params.add(new BasicNameValuePair("payload", json))
                                def postContent = new UrlEncodedFormEntity(params, "UTF-8")
                                httpPost.setEntity(postContent)
                                if (webHook.token.exists(_.trim.nonEmpty)) {
                                    // TODO find a better way and see how to extract content from postContent
                                    val contentAsBytes = URLEncodedUtils.format(params, "UTF-8")
                                        .getBytes("UTF-8")
                                    httpPost.addHeader(
                                      "X-Hub-Signature",
                                      XHub.generateHeaderXHubToken(
                                        XHubConverter.HEXA_LOWERCASE,
                                        XHubDigest.SHA1,
                                        webHook.token.get,
                                        contentAsBytes
                                      )
                                    )
                                }
                            }
                            case WebHookContentType.JSON => {
                                httpPost.setEntity(
                                  EntityBuilder.create()
                                      .setContentType(ContentType.APPLICATION_JSON).setText(json)
                                      .build()
                                )
                                if (webHook.token.exists(_.trim.nonEmpty)) {
                                    httpPost.addHeader(
                                      "X-Hub-Signature",
                                      XHub.generateHeaderXHubToken(
                                        XHubConverter.HEXA_LOWERCASE,
                                        XHubDigest.SHA1,
                                        webHook.token.orNull,
                                        json.getBytes("UTF-8")
                                      )
                                    )
                                }
                            }
                        }

                        val res = httpClient.execute(httpPost)
                        httpPost.releaseConnection()
                        logger.debug(s"end web hook invocation for ${webHook}")
                        res
                    } catch {
                        case e: Throwable => {
                            if (!reqPromise.isCompleted) { reqPromise.failure(e) }
                            throw e
                        }
                    }
                }
                f.onComplete {
                    case Success(_) => logger.debug(s"Success: web hook request to ${webHook.url}")
                    case Failure(t) => logger
                            .error(s"Failed: web hook request to ${webHook.url}", t)
                }
                (webHook, json, reqPromise.future, f)
            }
        } else { Nil }
    }
}

trait WebHookPullRequestService extends WebHookService {
    self: AccountService with RepositoryService with PullRequestService with IssuesService =>

    import WebHookService._
    // https://developer.github.com/v3/activity/events/types/#issuesevent
    def callIssuesWebHook(
        action: String,
        repository: RepositoryService.RepositoryInfo,
        issue: Issue,
        sender: Account,
        settings: SystemSettings
    )(implicit s: Session, context: JsonFormat.Context): Unit = {
        callWebHookOf(repository.owner, repository.name, WebHook.Issues, settings) {
            val assigneeUsers = getIssueAssignees(repository.owner, repository.name, issue.issueId)
            val users = getAccountsByUserNames(
              Set(repository.owner, issue.openedUserName) ++ assigneeUsers.map(_.assigneeUserName),
              Set(sender)
            )
            for {
                repoOwner <- users.get(repository.owner)
                issueUser <- users.get(issue.openedUserName)
            } yield {
                WebHookIssuesPayload(
                  action = action,
                  number = issue.issueId,
                  repository = ApiRepository(repository, ApiUser(repoOwner)),
                  issue = ApiIssue(
                    issue,
                    RepositoryName(repository),
                    ApiUser(issueUser),
                    assigneeUsers.flatMap(x => users.get(x.assigneeUserName)).map(ApiUser(_)),
                    getIssueLabels(repository.owner, repository.name, issue.issueId)
                        .map(ApiLabel(_, RepositoryName(repository))),
                    getApiMilestone(repository, issue.milestoneId getOrElse (0))
                  ),
                  sender = ApiUser(sender)
                )
            }
        }
    }

    def callPullRequestWebHook(
        action: String,
        repository: RepositoryService.RepositoryInfo,
        issueId: Int,
        sender: Account,
        settings: SystemSettings
    )(implicit s: Session, c: JsonFormat.Context): Unit = {
        import WebHookService._
        callWebHookOf(repository.owner, repository.name, WebHook.PullRequest, settings) {
            for {
                (issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
                assignees = getIssueAssignees(repository.owner, repository.name, issueId)
                users = getAccountsByUserNames(
                  Set(repository.owner, pullRequest.requestUserName, issue.openedUserName) ++
                      assignees.map(_.assigneeUserName),
                  Set(sender)
                )
                baseOwner <- users.get(repository.owner)
                headOwner <- users.get(pullRequest.requestUserName)
                issueUser <- users.get(issue.openedUserName)
                assigneeUsers = assignees.flatMap(x => users.get(x.assigneeUserName))
                headRepo <-
                    getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
                labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
                    .map(ApiLabel(_, RepositoryName(repository)))
            } yield {
                WebHookPullRequestPayload(
                  action = action,
                  issue = issue,
                  issueUser = issueUser,
                  assignees = assigneeUsers,
                  pullRequest = pullRequest,
                  headRepository = headRepo,
                  headOwner = headOwner,
                  baseRepository = repository,
                  baseOwner = baseOwner,
                  labels = labels,
                  sender = sender,
                  mergedComment = getMergedComment(repository.owner, repository.name, issueId)
                )
            }
        }
    }

    /** @return Map[(issue, issueUser, pullRequest, baseOwner, headOwner), webHooks] */
    def getPullRequestsByRequestForWebhook(
        userName: String,
        repositoryName: String,
        branch: String
    )(implicit
        s: Session
    ): Map[(Issue, Account, PullRequest, Account, Account), List[RepositoryWebHook]] = (for {
        is <- Issues if is.closed === false.bind
        pr <- PullRequests if pr.byPrimaryKey(is.userName, is.repositoryName, is.issueId)
        if pr.requestUserName === userName.bind
        if pr.requestRepositoryName === repositoryName.bind
        if pr.requestBranch === branch.bind
        bu <- Accounts if bu.userName === pr.userName
        ru <- Accounts if ru.userName === pr.requestUserName
        iu <- Accounts if iu.userName === is.openedUserName
        wh <- RepositoryWebHooks if wh.byRepository(is.userName, is.repositoryName)
        wht <- RepositoryWebHookEvents
        if wht.event === WebHook.PullRequest.asInstanceOf[WebHook.Event].bind &&
            wht.byRepositoryWebHook(wh)
    } yield { ((is, iu, pr, bu, ru), wh) }).list.groupBy(_._1).map { case (k, v) =>
        (k, v.map(_._2))
    }

    def callPullRequestWebHookByRequestBranch(
        action: String,
        requestRepository: RepositoryService.RepositoryInfo,
        requestBranch: String,
        sender: Account,
        settings: SystemSettings
    )(implicit s: Session, c: JsonFormat.Context): Unit = {
        import WebHookService._
        for {
            ((issue, issueUser, pullRequest, baseOwner, headOwner), webHooks) <-
                getPullRequestsByRequestForWebhook(
                  requestRepository.owner,
                  requestRepository.name,
                  requestBranch
                )
            assignees =
                getIssueAssignees(requestRepository.owner, requestRepository.name, issue.issueId)
                    .flatMap { x => getAccountByUserName(x.assigneeUserName, false) }
            baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName)
            labels = getIssueLabels(pullRequest.userName, pullRequest.repositoryName, issue.issueId)
                .map(ApiLabel(_, RepositoryName(pullRequest.userName, pullRequest.repositoryName)))
        } yield {
            val payload = WebHookPullRequestPayload(
              action = action,
              issue = issue,
              issueUser = issueUser,
              assignees = assignees,
              pullRequest = pullRequest,
              headRepository = requestRepository,
              headOwner = headOwner,
              baseRepository = baseRepo,
              baseOwner = baseOwner,
              labels = labels,
              sender = sender,
              mergedComment = getMergedComment(baseRepo.owner, baseRepo.name, issue.issueId)
            )

            callWebHook(WebHook.PullRequest, webHooks, payload, settings)
        }
    }

}

trait WebHookPullRequestReviewCommentService extends WebHookService {
    self: AccountService
        with RepositoryService with PullRequestService with IssuesService with CommitsService =>
    def callPullRequestReviewCommentWebHook(
        action: String,
        comment: CommitComment,
        repository: RepositoryService.RepositoryInfo,
        issue: Issue,
        pullRequest: PullRequest,
        sender: Account,
        settings: SystemSettings
    )(implicit s: Session, c: JsonFormat.Context): Unit = {
        import WebHookService._
        callWebHookOf(
          repository.owner,
          repository.name,
          WebHook.PullRequestReviewComment,
          settings
        ) {
            val assignees =
                getIssueAssignees(repository.owner, pullRequest.requestUserName, issue.issueId)
            val users = getAccountsByUserNames(
              Set(repository.owner, pullRequest.requestUserName, issue.openedUserName) ++
                  assignees.map(_.assigneeUserName),
              Set(sender)
            )
            for {
                baseOwner <- users.get(repository.owner)
                headOwner <- users.get(pullRequest.requestUserName)
                issueUser <- users.get(issue.openedUserName)
                assigneeUsers = assignees.flatMap(x => users.get(x.assigneeUserName))
                headRepo <-
                    getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
                labels = getIssueLabels(
                  pullRequest.userName,
                  pullRequest.repositoryName,
                  issue.issueId
                ).map(ApiLabel(_, RepositoryName(pullRequest.userName, pullRequest.repositoryName)))
            } yield {
                WebHookPullRequestReviewCommentPayload(
                  action = action,
                  comment = comment,
                  issue = issue,
                  issueUser = issueUser,
                  assignees = assigneeUsers,
                  pullRequest = pullRequest,
                  headRepository = headRepo,
                  headOwner = headOwner,
                  baseRepository = repository,
                  baseOwner = baseOwner,
                  labels = labels,
                  sender = sender,
                  mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
                )
            }
        }
    }
}

trait WebHookIssueCommentService extends WebHookPullRequestService {
    self: AccountService with RepositoryService with PullRequestService with IssuesService =>

    import WebHookService._
    def callIssueCommentWebHook(
        repository: RepositoryService.RepositoryInfo,
        issue: Issue,
        issueCommentId: Int,
        sender: Account,
        settings: SystemSettings
    )(implicit s: Session, c: JsonFormat.Context): Unit = {
        callWebHookOf(repository.owner, repository.name, WebHook.IssueComment, settings) {
            for {
                issueComment <-
                    getComment(repository.owner, repository.name, issueCommentId.toString())
                assignees = getIssueAssignees(repository.owner, repository.name, issue.issueId)
                users = getAccountsByUserNames(
                  Set(issue.openedUserName, repository.owner, issueComment.commentedUserName) ++
                      assignees.map(_.assigneeUserName),
                  Set(sender)
                )
                issueUser <- users.get(issue.openedUserName)
                repoOwner <- users.get(repository.owner)
                commenter <- users.get(issueComment.commentedUserName)
                assigneeUsers = assignees.flatMap(x => users.get(x.assigneeUserName))
                labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
                milestone = getApiMilestone(repository, issue.milestoneId getOrElse (0))
            } yield {
                WebHookIssueCommentPayload(
                  issue = issue,
                  issueUser = issueUser,
                  comment = issueComment,
                  commentUser = commenter,
                  repository = repository,
                  repositoryUser = repoOwner,
                  assignees = assigneeUsers,
                  sender = sender,
                  labels = labels,
                  milestone = milestone
                )
            }
        }
    }
}

object WebHookService {
    trait WebHookPayload

    // https://developer.github.com/v3/activity/events/types/#createevent
    case class WebHookCreatePayload(
        sender: ApiUser,
        description: String,
        ref: String,
        ref_type: String,
        master_branch: String,
        repository: ApiRepository
    ) extends FieldSerializable with WebHookPayload {
        val pusher_type = "user"
    }

    object WebHookCreatePayload {

        def apply(
            sender: Account,
            repositoryInfo: RepositoryInfo,
            repositoryOwner: Account,
            ref: String,
            refType: String
        ): WebHookCreatePayload = WebHookCreatePayload(
          sender = ApiUser(sender),
          ref = ref,
          ref_type = refType,
          description = repositoryInfo.repository.description.getOrElse(""),
          master_branch = repositoryInfo.repository.defaultBranch,
          repository = ApiRepository(repositoryInfo, repositoryOwner)
        )
    }

    // https://developer.github.com/v3/activity/events/types/#pushevent
    case class WebHookPushPayload(
        pusher: ApiPusher,
        sender: ApiUser,
        ref: String,
        before: String,
        after: String,
        commits: List[ApiCommit],
        repository: ApiRepository
    ) extends FieldSerializable with WebHookPayload {
        val compare = commits.size match {
            case 0 =>
                ApiPath(s"/${repository.full_name}") // maybe test hook on un-initialized repository
            case 1 => ApiPath(s"/${repository.full_name}/commit/${after}")
            case _ if before.forall(_ == '0') =>
                ApiPath(s"/${repository.full_name}/compare/${commits.head.id}^...${after}")
            case _ => ApiPath(s"/${repository.full_name}/compare/${before}...${after}")
        }
        val head_commit = commits.lastOption
    }

    object WebHookPushPayload {
        def apply(
            git: Git,
            sender: Account,
            refName: String,
            repositoryInfo: RepositoryInfo,
            commits: List[CommitInfo],
            repositoryOwner: Account,
            newId: ObjectId,
            oldId: ObjectId
        ): WebHookPushPayload = WebHookPushPayload(
          pusher = ApiPusher(sender),
          sender = ApiUser(sender),
          ref = refName,
          before = ObjectId.toString(oldId),
          after = ObjectId.toString(newId),
          commits = commits.map { commit =>
              ApiCommit(git, RepositoryName(repositoryInfo), commit)
          },
          repository = ApiRepository(repositoryInfo, repositoryOwner)
        )

        def createDummyPayload(sender: Account): WebHookPushPayload = WebHookPushPayload(
          pusher = ApiPusher(sender),
          sender = ApiUser(sender),
          ref = "refs/heads/master",
          before = "adc83b19e793491b1c6ea0fd8b46cd9f32e592fc",
          after = "adc83b19e793491b1c6ea0fd8b46cd9f32e592fc",
          commits = List.empty,
          repository = ApiRepository.forDummyPayload(ApiUser(sender))
        )
    }

    // https://developer.github.com/v3/activity/events/types/#issuesevent
    case class WebHookIssuesPayload(
        action: String,
        number: Int,
        repository: ApiRepository,
        issue: ApiIssue,
        sender: ApiUser
    ) extends WebHookPayload

    // https://developer.github.com/v3/activity/events/types/#pullrequestevent
    case class WebHookPullRequestPayload(
        action: String,
        number: Int,
        repository: ApiRepository,
        pull_request: ApiPullRequest,
        sender: ApiUser
    ) extends WebHookPayload

    object WebHookPullRequestPayload {
        def apply(
            action: String,
            issue: Issue,
            issueUser: Account,
            assignees: List[Account],
            pullRequest: PullRequest,
            headRepository: RepositoryInfo,
            headOwner: Account,
            baseRepository: RepositoryInfo,
            baseOwner: Account,
            labels: List[ApiLabel],
            sender: Account,
            mergedComment: Option[(IssueComment, Account)]
        ): WebHookPullRequestPayload = {

            val headRepoPayload = ApiRepository(headRepository, headOwner)
            val baseRepoPayload = ApiRepository(baseRepository, baseOwner)
            val senderPayload = ApiUser(sender)
            val pr = ApiPullRequest(
              issue = issue,
              pullRequest = pullRequest,
              headRepo = headRepoPayload,
              baseRepo = baseRepoPayload,
              user = ApiUser(issueUser),
              labels = labels,
              assignees = assignees.map(ApiUser.apply),
              mergedComment = mergedComment
            )

            WebHookPullRequestPayload(
              action = action,
              number = issue.issueId,
              repository = pr.base.repo,
              pull_request = pr,
              sender = senderPayload
            )
        }
    }

    // https://developer.github.com/v3/activity/events/types/#issuecommentevent
    case class WebHookIssueCommentPayload(
        action: String,
        repository: ApiRepository,
        issue: ApiIssue,
        comment: ApiComment,
        sender: ApiUser
    ) extends WebHookPayload

    object WebHookIssueCommentPayload {
        def apply(
            issue: Issue,
            issueUser: Account,
            comment: IssueComment,
            commentUser: Account,
            repository: RepositoryInfo,
            repositoryUser: Account,
            assignees: List[Account],
            sender: Account,
            labels: List[Label],
            milestone: Option[ApiMilestone]
        ): WebHookIssueCommentPayload = WebHookIssueCommentPayload(
          action = "created",
          repository = ApiRepository(repository, repositoryUser),
          issue = ApiIssue(
            issue,
            RepositoryName(repository),
            ApiUser(issueUser),
            assignees.map(ApiUser(_)),
            labels.map(ApiLabel(_, RepositoryName(repository))),
            milestone
          ),
          comment = ApiComment(
            comment,
            RepositoryName(repository),
            issue.issueId,
            ApiUser(commentUser),
            issue.isPullRequest
          ),
          sender = ApiUser(sender)
        )
    }

    // https://developer.github.com/v3/activity/events/types/#pullrequestreviewcommentevent
    case class WebHookPullRequestReviewCommentPayload(
        action: String,
        comment: ApiPullRequestReviewComment,
        pull_request: ApiPullRequest,
        repository: ApiRepository,
        sender: ApiUser
    ) extends WebHookPayload

    object WebHookPullRequestReviewCommentPayload {
        def apply(
            action: String,
            comment: CommitComment,
            issue: Issue,
            issueUser: Account,
            assignees: List[Account],
            pullRequest: PullRequest,
            headRepository: RepositoryInfo,
            headOwner: Account,
            baseRepository: RepositoryInfo,
            baseOwner: Account,
            labels: List[ApiLabel],
            sender: Account,
            mergedComment: Option[(IssueComment, Account)]
        ): WebHookPullRequestReviewCommentPayload = {
            val headRepoPayload = ApiRepository(headRepository, headOwner)
            val baseRepoPayload = ApiRepository(baseRepository, baseOwner)
            val senderPayload = ApiUser(sender)

            WebHookPullRequestReviewCommentPayload(
              action = action,
              comment = ApiPullRequestReviewComment(
                comment = comment,
                commentedUser = senderPayload,
                repositoryName = RepositoryName(baseRepository),
                issueId = issue.issueId
              ),
              pull_request = ApiPullRequest(
                issue = issue,
                pullRequest = pullRequest,
                headRepo = headRepoPayload,
                baseRepo = baseRepoPayload,
                user = ApiUser(issueUser),
                labels = labels,
                assignees = assignees.map(ApiUser.apply),
                mergedComment = mergedComment
              ),
              repository = baseRepoPayload,
              sender = senderPayload
            )
        }
    }

    // https://developer.github.com/v3/activity/events/types/#gollumevent
    case class WebHookGollumPayload(
        pages: Seq[WebHookGollumPagePayload],
        repository: ApiRepository,
        sender: ApiUser
    ) extends WebHookPayload

    case class WebHookGollumPagePayload(
        page_name: String,
        title: String,
        summary: Option[String] = None,
        action: String, // created or edited
        sha: String, // SHA of the latest commit
        html_url: ApiPath
    )

    object WebHookGollumPayload {
        def apply(
            action: String,
            pageName: String,
            sha: String,
            repository: RepositoryInfo,
            repositoryUser: Account,
            sender: Account
        ): WebHookGollumPayload =
            apply(Seq((action, pageName, sha)), repository, repositoryUser, sender)

        def apply(
            pages: Seq[(String, String, String)],
            repository: RepositoryInfo,
            repositoryUser: Account,
            sender: Account
        ): WebHookGollumPayload = {
            WebHookGollumPayload(
              pages = pages.map { case (action, pageName, sha) =>
                  WebHookGollumPagePayload(
                    action = action,
                    page_name = pageName,
                    title = pageName,
                    sha = sha,
                    html_url = ApiPath(
                      s"/${RepositoryName(repository).fullName}/wiki/${StringUtil.urlDecode(pageName)}"
                    )
                  )
              },
              repository = ApiRepository(repository, repositoryUser),
              sender = ApiUser(sender)
            )
        }
    }

}
